#include "jni_player.h"

#ifdef __cplusplus
extern "C"
{
#endif

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <libavutil/timestamp.h>
#include <libavutil/imgutils.h>
#include <libavutil/samplefmt.h>

#ifdef __cplusplus
};
#endif

static const char* const kClassPathName = "com/media/ffmpeg/FFMpegPlayer";

struct fields_t {
    jfieldID    mp_context;
    jmethodID   native_event;
    jmethodID   updateVideoFrame_mid;
};

static fields_t fields;

void init(JNIEnv *env, jobject thiz, jobject weak_thiz) {

    jclass clazz;
    clazz = env->FindClass(kClassPathName);
    if (clazz == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "Can't find android/media/MediaPlayer");
        return;
    }

    fields.mp_context = env->GetFieldID(clazz, "mNativeContext", "I");
    if (fields.mp_context == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "Can't find MediaPlayer.mNativeContext");
        return;
    }

    fields.native_event = env->GetMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIIILjava/lang/Object;)V");
    if (fields.native_event == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "Can't find FFMpegMediaPlayer.postEventFromNative");
        return;
    }

    fields.updateVideoFrame_mid = env->GetMethodID(clazz, "postEventVideoFrameUpdated", "()V");
    if (fields.updateVideoFrame_mid == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "Can't find FFMpegMediaPlayer.postEventVideoFrameUpdated");
        return;
    }

    globalThizRef = env->NewGlobalRef(thiz);

    LOGD("init");

    av_register_all();

    initMediaPlayer(env, thiz, weak_thiz);

    pthread_mutex_init(&mLock, NULL);
    pFrame = NULL;
    mFrameUpdated = false;

//    video_dst_file = fopen("/mnt/sdcard/testD.yuv", "wb");
//    if (!video_dst_file) {
//        LOGE("Could not open destination video file\n");
//    }

    video_dst_data[4] = {NULL};
    mImageAllocated = false;
    mSeq = 1;

    startRefresh();
}

void initMediaPlayer(JNIEnv* env, jobject thiz, jobject weak_thiz) {
    LOGD("init mediaplayer");

    MediaPlayer* mp = new MediaPlayer();

    // create new listener and give it to MediaPlayer
    JNIFFmpegMediaPlayerListener* listener = new JNIFFmpegMediaPlayerListener(env, thiz, weak_thiz);
    mp->setListener(listener);
    mp->setDecodeCallback(onVideoDecoded);

    setMediaPlayer(env, thiz, mp);
}

MediaPlayer* getMediaPlayer(JNIEnv* env, jobject thiz) {
    return (MediaPlayer*)env->GetIntField(thiz, fields.mp_context);
}

MediaPlayer* setMediaPlayer(JNIEnv* env, jobject thiz, MediaPlayer* player)
{
    MediaPlayer* old = (MediaPlayer*)env->GetIntField(thiz, fields.mp_context);
    if (old != NULL) {
        LOGD("freeing old mediaplayer object");
        free(old);
    }
    env->SetIntField(thiz, fields.mp_context, (int)player);
    return old;
}

void setDataSource(JNIEnv *env, jobject thiz, jstring path) {
    const char* src_file = env->GetStringUTFChars(path, NULL);
    if (src_file == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
        return;
    }

    LOGD("setDataSource: path %s", src_file);

    MediaPlayer* mp = getMediaPlayer(env, thiz);
    status_t opStatus = mp->setDataSource(src_file);

    env->ReleaseStringUTFChars(path, src_file);

    process_media_player_call(
            env, thiz, opStatus, "java/io/IOException",
            "setDataSource failed." );
}

void prepare(JNIEnv *env, jobject thiz) {
    MediaPlayer* mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return;
    }
    process_media_player_call( env, thiz, mp->prepare(), "java/io/IOException", "Prepare failed." );
}

void start(JNIEnv *env, jobject thiz) {
    MediaPlayer* mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return;
    }
    process_media_player_call(env, thiz, mp->start(), NULL, NULL);
}

void stop(JNIEnv *env, jobject thiz) {
    MediaPlayer* mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return;
    }
    process_media_player_call( env, thiz, mp->stop(), NULL, NULL);
}

void seekTo(JNIEnv *env, jobject thiz, jlong msec) {
    MediaPlayer* mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return;
    }
    process_media_player_call( env, thiz, mp->seekTo(msec), NULL, NULL);
}

jboolean isPlaying(JNIEnv *env, jobject thiz) {
    MediaPlayer* mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return JNI_FALSE;
    }
    const jboolean is_playing = mp->isPlaying();
    return is_playing;
}

jint getVideoWidth(JNIEnv *env, jobject thiz)
{
    MediaPlayer* mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return 0;
    }
    return mp->getVideoWidth();
}

jint getVideoHeight(JNIEnv *env, jobject thiz)
{
    MediaPlayer* mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return 0;
    }
    return mp->getVideoHeight();
}

jlong getCurrentPosition(JNIEnv *env, jobject thiz)
{
    MediaPlayer* mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return 0;
    }
    return mp->getCurrentPosition();
}

jlong getDuration(JNIEnv *env, jobject thiz)
{
    MediaPlayer* mp = getMediaPlayer(env, thiz);
    if (mp == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return 0;
    }
    return mp->getCurrentPosition();
}

void initViewPort(JNIEnv *env, jobject thiz, jint rotate, jint width, jint height)
{
    gl_init(rotate, width, height);
}

void step(JNIEnv *env, jobject thiz)
{
    pthread_mutex_lock(&mLock);

    if (!mFrameList.empty() && mImageAllocated) {

        AVFrameRef* frameRef = mFrameList.front();

        LOGD("----------------------------SEQ:%d", frameRef->seq);

        av_image_copy(video_dst_data, video_dst_linesize,
                      (const uint8_t **) (frameRef->frame->data), frameRef->frame->linesize,
                      AV_PIX_FMT_YUV420P, frameRef->frame->width, frameRef->frame->height);

//        fwrite(video_dst_data[0], 1, video_dst_bufsize, video_dst_file);

//        gl_render(reinterpret_cast<char *>(video_dst_data[0]), mVideoWidth, mVideoHeight);

//        av_frame_unref(frameRef->frame);
        av_frame_free(&frameRef->frame);
        mFrameList.pop_front();
        delete frameRef;

    }

    pthread_mutex_unlock(&mLock);



    /*if (mFrameUpdated) {

        pthread_mutex_lock(&mLock);

        gl_render(pFrame, mVideoWidth, mVideoHeight);

        pthread_mutex_unlock(&mLock);

        mFrameUpdated = false;
    }*/
}

JNIFFmpegMediaPlayerListener::JNIFFmpegMediaPlayerListener() {

}

JNIFFmpegMediaPlayerListener::JNIFFmpegMediaPlayerListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
{
    // Hold onto the MediaPlayer class for use in calling the static method
    // that posts events to the application thread.
    jclass clazz = env->GetObjectClass(thiz);
    if (clazz == NULL) {
        jniThrowException(env, "java/lang/Exception", kClassPathName);
        return;
    }
    mClass = (jclass)env->NewGlobalRef(clazz);

    // We use a weak reference so the MediaPlayer object can be garbage collected.
    // The reference is only used as a proxy for callbacks.
    mObject  = env->NewGlobalRef(weak_thiz);
}

JNIFFmpegMediaPlayerListener::~JNIFFmpegMediaPlayerListener()
{
    // remove global references
    JNIEnv *env = getJNIEnv();
    env->DeleteGlobalRef(mObject);
    env->DeleteGlobalRef(mClass);
}

void JNIFFmpegMediaPlayerListener::notify(int msg, int ext1, int ext2, int ext3)
{
    JNIEnv *env = getJNIEnv();
    env->CallVoidMethod(globalThizRef, fields.native_event, mObject, msg, ext1, ext2, ext3, NULL);
}


void onVideoDecoded(AVFrame* frame, double pts, int width, int height) {
    mVideoWidth = width;
    mVideoHeight = height;

    if (!mImageAllocated) {
        video_dst_bufsize = av_image_alloc(video_dst_data, video_dst_linesize,
                                           mVideoWidth, mVideoHeight, AV_PIX_FMT_YUV420P, 1);

        if (video_dst_bufsize < 0) {
            LOGE("Could not allocate raw video buffer\n");
            return;
        }
        mImageAllocated = true;
    }

    pthread_mutex_lock(&mLock);

    AVFrameRef* frameRef = new AVFrameRef;
    frameRef->frame = frame;
//    av_frame_get_buffer(frameRef->frame, 0);
//    av_frame_copy(frameRef->frame, frame);
//    av_frame_copy_props(frameRef->frame, frame);

//    av_frame_ref(frameRef->frame, frame);

    frameRef->pts = pts;
    frameRef->seq = mSeq;

    mSeq++;

    mFrameList.push_back(frameRef);

    pthread_mutex_unlock(&mLock);


    /*
     pthread_mutex_lock(&mLock);
    int size = width * height * 3 / 2;
    if (pFrame == NULL) {
        pFrame = static_cast<char *>(malloc(size));
    }

    memcpy(pFrame, yuv, size);

    mFrameUpdated = true;

    pthread_mutex_unlock(&mLock);

    JNIEnv *env;
    gVm->AttachCurrentThread(&env, NULL);
    if (env == NULL) {
        LOGD("getJNIEnv fail -------------------------");
        return;
    }
    env->CallVoidMethod(globalThizRef, fields.updateVideoFrame_mid);

     */

//    gVm->DetachCurrentThread();
}

void startRefresh() {
    pthread_create(&mRefreshThread, NULL, refreshTimer, NULL);
}

void* refreshTimer(void* ptr) {
    JNIEnv *env;
    gVm->AttachCurrentThread(&env, NULL);
    if (env == NULL) {
        LOGD("getJNIEnv fail -------------------------");
        return NULL;
    }
    while (1) {
        LOGD("refreshTimer -------------------------");
        env->CallVoidMethod(globalThizRef, fields.updateVideoFrame_mid);

        usleep(1000 * 100);
    }


}

// If exception is NULL and opStatus is not OK, this method sends an error
// event to the client application; otherwise, if exception is not NULL and
// opStatus is not OK, this method throws the given exception to the client
// application.
void process_media_player_call(JNIEnv *env, jobject thiz, status_t opStatus, const char* exception, const char *message)
{
    if (exception == NULL) {  // Don't throw exception. Instead, send an event.

        /*if (opStatus != (status_t) OK) {
            MediaPlayer* mp = getMediaPlayer(env, thiz);
            if (mp != 0) mp->notify(MEDIA_ERROR, opStatus, 0);
        }*/

    } else {  // Throw exception!
        if ( opStatus == (status_t) INVALID_OPERATION ) {
            jniThrowException(env, "java/lang/IllegalStateException", NULL);
        } else if ( opStatus != (status_t) OK ) {
            if (strlen(message) > 230) {
                // if the message is too long, don't bother displaying the status code
                jniThrowException( env, exception, message);
            } else {
                char msg[256];
                // append the status code to the message
                sprintf(msg, "%s: status=0x%X", message, opStatus);
                jniThrowException( env, exception, msg);
            }
        }
    }
}


static JNINativeMethod gMethods[] = {
        {"_init",                "(Ljava/lang/Object;)V",            (void *)init},
        {"_setDataSource",       "(Ljava/lang/String;)V",            (void *)setDataSource},
        {"_prepare",             "()V",                              (void *)prepare},
        {"_start",               "()V",                              (void *)start},
        {"_seekTo",              "(J)V",                             (void *)seekTo},
        {"_stop",                "()V",                              (void *)stop},
        {"_isPlaying",           "()Z",                              (void *)isPlaying},
        {"_getVideoWidth",       "()I",                              (void *)getVideoWidth},
        {"_getVideoHeight",      "()I",                              (void *)getVideoHeight},
        {"_getCurrentPosition",  "()J",                              (void *)getCurrentPosition},
        {"_getDuration",         "()J",                              (void *)getDuration},
        {"_initViewPort",        "(III)V",                           (void *)initViewPort},
        {"_step",                "()V",                              (void *)step}
};



jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = NULL;
    jint result = JNI_ERR;
    gVm = vm;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        LOGD("GetEnv failed!");
        return result;
    }

    jniRegisterNativeMethods(env, kClassPathName, gMethods, sizeof(gMethods) / sizeof(gMethods[0]));

    return JNI_VERSION_1_4;
}

void JNI_OnUnload(JavaVM* vm, void* reserved) {
    JNIEnv *env = getJNIEnv();
    env->DeleteGlobalRef(globalThizRef);
    if (mImageAllocated) {
        av_free(video_dst_data[0]);
    }
}

int jniRegisterNativeMethods(JNIEnv* env,
                             const char* className,
                             const JNINativeMethod* gMethods,
                             int numMethods)
{
    jclass clazz;

    LOGD("Registering %s natives\n", className);
    clazz = env->FindClass(className);
    if (clazz == NULL) {
        LOGD("Native registration unable to find class '%s'\n", className);
        return -1;
    }
    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        LOGD("RegisterNatives failed for '%s'\n", className);
        return -1;
    }
    return 0;
}


JNIEnv* getJNIEnv() {
    JNIEnv* env = NULL;
    if (gVm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        LOGD("Failed to obtain JNIEnv");
        return NULL;
    }
    return env;
}

/*
 * Throw an exception with the specified class and an optional message.
 */
int jniThrowException(JNIEnv* env, const char* className, const char* msg) {
    jclass exceptionClass = env->FindClass(className);
    if (exceptionClass == NULL) {
        LOGD("jniThrowException --------------------Unable to find exception class %s", className);
        return -1;
    }

    if (env->ThrowNew(exceptionClass, msg) != JNI_OK) {
       LOGD("jniThrowException -------------------Failed throwing '%s' '%s'", className, msg);
    }
    return 0;
}
